Writing custom DB engines

FastReport allows building reports not only on the basis of data defined in the application. You can define your own data sources (connections to DB, queries) right in the report as well. FastReport is supplied with engines for ADO, BDE, IBX. You can create your own engine, and then connect it to FastReport.

The picture below shows the hierarchy of classes intended for creating DB engines. The components of a new engine are highlighted with green color.

As you can see, a standard set of the DB engine’s components includes Database, Table and Query. You can realize all these components or some of them (for example, many DB have no component of the Table type). You can also realize components, which are not included into the standard set (for example, the StoredProc analogue).

Let us examine basic classes in detail.

TfrxDialogComponent is a basic class for all non-visual components, which can be placed into the FastReport dialogue form. There are no any important properties or methods defined in it.

TfrxCustomDataSet is a basic class of DB components derived from TDataSet.

The components inherited from this class are clones of “Query,” “Table,” and “StoredProc.” As a matter of fact, the class represents a cover over TDataSet.

TfrxCustomDataset = class (TfrxDBDataSet)

protected

procedure SetMaster( const Value: TDataSource); virtual ;

procedure SetMasterFields( const Value: String ); virtual ;

public

property DataSet: TDataSet;

property Fields: TFields readonly;

property MasterFields: String ;

property Active: Boolean;

published

property Filter: String ;

property Filtered: Boolean;

property Master: TfrxDBDataSet;

end;

The following properties are defined in the class:

- DataSet is a link to the buried object of the “TdataSet” type;

- Fields is a link to the DataSet.Fields;

- Active - whether a data set is active;

- Filter - expression for filtering;

- Filtered – whether filtering is active;

- Master is a link to master dataset in a master-detail relationship.

- MasterFields is a list of fields like field1=field2. Used for master-detail relations.

A component of the Table type inherits from the given class. For its realization, it is necessary to define lacking properties; as a rule, they are: “Database,” “IndexName,” and “TableName.” Also you should override SetMaster, SetMasterFields methods to allow master-detail relations.

TfrxCustomQuery is a basic class for DB components of the “Query” type. The class is a cover for a Query type component.

TfrxCustomQuery = class (TfrxCustomDataset)

protected

procedure SetSQL(Value: TStrings); virtual ; abstract ;

function GetSQL: TStrings; virtual ; abstract ;

public

procedure UpdateParams; virtual ; abstract ;

published

property Params: TfrxParams;

property SQL: TStrings;

end;

The “SQL” and “Params” properties (which are general for all Query components) are defined in the class. Since different Query components have different realization of parameters (for example, TParams and TParameters), the “Params” property has the “TfrxParams” type and is a cover for the concrete parameters’ type.

The following methods are defined in this class:

- SetSQL is to set the “SQL” property of the component of the “Query” type;

- GetSQL is to get the “SQL” property of the component of the “Query” type;

- UpdateParams is to copy parameters’ values into the component of the Query type. If a Query component’s parameters are of the TParams type, copying is performed via the frxParamsToTParams standard procedure.

Let us illustrate creation process of the DB engine by the IBX example. The full original text of the engine can be found in the SOURCE\IBX directory. Below are some quotations from the source text with our comments.

The IBX components around which we will build a cover are: TIBDatabase, TIBTable, and TIBQuery. Accordingly, our components will be named “TfrxIBXDatabase,” “TfrxIBXTable,” and “TfrxIBXQuery.”

“TfrxIBXComponents” is another component we should create; it will be placed into the FastReport component palette when registering the engine (in Delphi environment). As soon as this component is placed into a project, Delphi automatically adds a link to the unit of our engine into the “Uses” list. It is convenient to assign one more task in this component, i.e. to define the “DefaultDatabase” property in it, which refers to the existing connection to DB. By default, all the TfrxIBXTable and TfrxIBXQuery components will refer to this connection. It is necessary to inherit the component from the TfrxDBComponents class:

TfrxDBComponents = class (TComponent)

public

function GetDescription: String ; virtual ; abstract ;

end;

The description should be returned by one function only, for example ”IBX Components.” We plan to add new methods to the list in the future in order to provide support of the visual query builders. Realization of the “TfrxIBXComponents” component is as following:

type

TfrxIBXComponents = class (TfrxDBComponents)

private

FDefaultDatabase: TIBDatabase;

FOldComponents: TfrxIBXComponents;

public

constructor Create(AOwner: TComponent); override ;

destructor Destroy; override ;

function GetDescription: String ; override ;

published

property DefaultDatabase: TIBDatabase read FDefaultDatabase write FDefaultDatabase;

end;

var

IBXComponents: TfrxIBXComponents;

constructor TfrxIBXComponents.Create(AOwner: TComponent);

begin

inherited ;

FOldComponents := IBXComponents;

IBXComponents := Self;

end;

destructor TfrxIBXComponents.Destroy;

begin

if IBXComponents = Self then

IBXComponents := FOldComponents;

inherited ;

end;

functionTfrxIBXComponents.GetDescription: String ;

begin

Result := 'IBX';

end;

We define the IBXComponents global variable, which will refer to the TfrxIBXComponents component’s copy. If you place the component into the project several times (though it is senseless), you will nevertheless be able to save the link to the previous component and restore it after deleting the component.

A link to the connection to DB, which already exists in the project, can be placed into the “DefaultDatabase” property. The way we will write the TfrxIBXTable, TfrxIBXQuery components allows them using this connection by default (actually, this is what we need the IBXComponents global variable for).

The following component is the TfrxIBXDatabase one. It represents a cover over the TIBDatabase.

TfrxIBXDatabase = class (TfrxDialogComponent)

private

FDatabase: TIBDatabase;

FTransaction: TIBTransaction;

procedure SetConnected(Value: Boolean);

procedure SetDatabaseName( const Value: String );

procedure SetLoginPrompt(Value: Boolean);

procedure SetParams(Value: TStrings);

function GetConnected: Boolean;

function GetDatabaseName: String ;

function GetLoginPrompt: Boolean;

function GetParams: TStrings;

function GetSQLDialect: Integer;

procedure SetSQLDialect( const Value: Integer);

public

constructor Create(AOwner: TComponent); override ;

destructor Destroy; override ;

class function GetDescription: String ; override ;

property Database: TIBDatabase read FDatabase;

published

{ list TIBDatabase properties }

property DatabaseName: String read GetDatabaseName write

SetDatabaseName;

property LoginPrompt: Boolean read GetLoginPrompt write

SetLoginPrompt default True;

property Params: TStrings read GetParams write SetParams;

property Connected: Boolean read GetConnected write SetConnected

default False;

property SQLDialect: Integer read GetSQLDialect write SetSQLDialect;

end;

constructor TfrxIBXDatabase.Create(AOwner: TComponent);

begin

inherited ;

{ create a component – connection }

FDatabase := TIBDatabase.Create( nil );

{ create a component - transaction (specificity of the IBX) }

FTransaction := TIBTransaction.Create( nil );

FDatabase.DefaultTransaction := FTransaction;

{ do not forget this string! }

Component := FDatabase;

{ component’s icon – take it from the standard set }

FImageIndex := 37;

end;

destructor TfrxIBXDatabase.Destroy;

begin

{ delete the transaction }

FTransaction.Free;

{ the connection will be deleted automatically in the parent class }

inherited ;

end;

{ component’s description will be displayed next to the icon in the objects toolbar }

class function TfrxIBXDatabase.GetDescription: String ;

begin

Result := 'IBX Database';

end;

{ redirect component’s properties to the cover’s properties and vice versa }

function TfrxIBXDatabase.GetConnected: Boolean;

begin

Result := FDatabase.Connected;

end;

function TfrxIBXDatabase.GetDatabaseName: String ;

begin

Result := FDatabase.DatabaseName;

end;

function TfrxIBXDatabase.GetLoginPrompt: Boolean;

begin

Result := FDatabase.LoginPrompt;

end;

function TfrxIBXDatabase.GetParams: TStrings;

begin

Result := FDatabase.Params;

end;

procedure TfrxIBXDatabase.SetConnected(Value: Boolean);

begin

FDatabase.Connected := Value;

FTransaction.Active := Value;

end;

procedure TfrxIBXDatabase.SetDatabaseName( const Value: String );

begin

FDatabase.DatabaseName := Value;

end;

procedure TfrxIBXDatabase.SetLoginPrompt(Value: Boolean);

begin

FDatabase.LoginPrompt := Value;

end;

procedure TfrxIBXDatabase.SetParams(Value: TStrings);

begin

FDatabase.Params := Value;

end;

function TfrxIBXDatabase.GetSQLDialect: Integer;

begin

Result := FDatabase.SQLDialect;

end;

procedure TfrxIBXDatabase.SetSQLDialect( const Value: Integer);

begin

FDatabase.SQLDialect := Value;

end;

As you can see, this is not that complicated. We create the FDatabase:

“TIBDatabase” object, and then define properties we want the designer to possess. The “Get” and “Set” methods are written for each property.

The next class is TfrxIBXTable. It inherits, as it was mentioned above, from the TfrxCustomDataSet standard class. All basic functionality (operating with the list of fields, master-detail, basic properties) is already realized in the basic class. We only need to define properties, which are specific for the given component.

TfrxIBXTable = class (TfrxCustomDataset)

private

FDatabase: TfrxIBXDatabase;

FTable: TIBTable;

procedure SetIndexName( const Value: String );

function GetIndexName: String ;

function GetTableName: String ;

procedure SetTableName( const Value: String );

procedure SetDatabase( const Value: TfrxIBXDatabase);

protected

procedure SetMaster( const Value: TDataSource); override ;

procedure SetMasterFields( const Value: String ); override ;

public

constructor Create(AOwner: TComponent); override ;

class function GetDescription: String; override ;

property Table: TIBTable read FTable;

published

property Database: TfrxIBXDatabase read FDatabase write SetDatabase;

property IndexName: String read GetIndexName write SetIndexName;

property TableName: String read GetTableName write SetTableName;

end;

constructor TfrxIBXTable.Create(AOwner: TComponent);

begin

{ create a component – a table }

FTable := TIBTable.Create( nil );

{ assign a link to the DataSet property from the basic class – do not forget this string! }

DataSet := FTable;

{ assign a link to connection to DB by default }

SetDatabase( nil );

{ after that the basic constructor may be called in}

inherited ;

{ component’s icon; we take it from the standard set }

FImageIndex := 38;

end;

class function TfrxIBXTable.GetDescription: String ;

begin

Result := 'IBX Table';

end;

procedure TfrxIBXTable.SetDatabase( const Value: TfrxIBXDatabase);

begin

{ the Database property of the TfrxIBXDatabase type, and not of the TIBDatabase one! }

FDatabase := Value;

{ if a value <> nil, connect a table to the selected component }

if Value <> nil then

FTable.Database := Value.Database

{ otherwise, try to connect to DB by default, defined in the

TfrxIBXComponents component }

else if IBXComponents <> nil then

FTable.Database := IBXComponents.DefaultDatabase

{ if there were no TfrxIBXComponents for some reason, reset to nil }

else

FTable.Database := nil ;

end;

function TfrxIBXTable.GetIndexName: String ;

begin

Result := FTable.IndexName;

end;

function TfrxIBXTable.GetTableName: String ;

begin

Result := FTable.TableName;

end;

procedure TfrxIBXTable.SetIndexName( const Value: String );

begin

FTable.IndexName := Value;

end;

procedure TfrxIBXTable.SetTableName( const Value: String );

begin

FTable.TableName := Value;

end;

procedure TfrxIBXTable.SetMaster( const Value: TDataSource);

begin

FTable.MasterSource := Value;

end;

procedure TfrxIBXTable.SetMasterFields( const Value: String );

begin

FTable.MasterFields := Value;

end;

Finally, lets examine the last component, “TfrxIBXQuery”. It inherits from the TfrxCustomQuery basic class, in which the necessary properties are already defined. We only need to define the Database property and override the SetMaster method.

TfrxIBXQuery = class (TfrxCustomQuery)

private

FDatabase: TfrxIBXDatabase;

FQuery: TIBQuery;

procedure SetDatabase( const Value: TfrxIBXDatabase);

protected

procedure SetMaster( const Value: TDataSource); override ;

procedure SetSQL(Value: TStrings); override ;

function GetSQL: TStrings; override ;

public

constructor Create(AOwner: TComponent); override ;

class function GetDescription: String ; override ;

procedure UpdateParams; override ;

property Query: TIBQuery read FQuery;

published

property Database: TfrxIBXDatabase read FDatabase write SetDatabase;

end;

constructor TfrxIBXQuery.Create(AOwner: TComponent);

begin

{ create a component – query }

FQuery := TIBQuery.Create( nil );

{ assign a link to it to the DataSet property from the basic class – do not forget this line! }

Dataset := FQuery;

{ assign a link to the connection to DB by default }

SetDatabase( nil );

{ after that a basic constructor may be called in }

inherited ;

{ component’s icon – take it from the standard set }

FImageIndex := 39;

end;

class function TfrxIBXQuery.GetDescription: String ;

begin

Result := 'IBX Query';

end;

procedure TfrxIBXQuery.SetDatabase( const Value: TfrxIBXDatabase);

begin

{ realization is analogical to TfrxIBXTable.SetDatabase }

FDatabase := Value;

if Value <> nil then

FQuery.Database := Value.Database

else if IBXComponents <> nil then

FQuery.Database := IBXComponents.DefaultDatabase

else

FQuery.Database := nil ;

end;

procedure TfrxIBXQuery.SetMaster( const Value: TDataSource);

begin

FQuery.DataSource := Value;

end;

function TfrxIBXQuery.GetSQL: TStrings;

begin

Result := FQuery.SQL;

end;

procedure TfrxIBXQuery.SetSQL(Value: TStrings);

begin

FQuery.SQL := Value;

end;

procedure TfrxIBXQuery.UpdateParams;

begin

{ in this method it is sufficient to assign values from Params into FQuery.Params }

{ this is performed via the standard procedure }

frxParamsToTParams(Self, FQuery.Params);

end;

Registration of all engine’s components is performed in the “Initialization” section. The category, where all the components are placed, is registered in the first place.

var

CatBmp: TBitmap;

initialization

CatBmp := TBitmap.Create;

CatBmp.LoadFromResourceName(hInstance, 'frxIBX');

frxObjects.RegisterCategory('IBX', CatBmp, 'IBX Components');

{ use indexes of standard pictures 37,38,39 instead of pictures}

frxObjects.RegisterObject1(TfrxIBXDataBase, nil , '', 'IBX', 0, 37);

frxObjects.RegisterObject1(TfrxIBXTable, nil , '', 'IBX', 0, 38);

frxObjects.RegisterObject1(TfrxIBXQuery, nil , '', 'IBX', 0, 39);

finalization

CatBmp.Free;

frxObjects.Unregister(TfrxIBXDataBase);

frxObjects.Unregister(TfrxIBXTable);

frxObjects.Unregister(TfrxIBXQuery);

end.

It is quite enough for using the engine in reports. There are two more things left at this stage: to register engine’s classes in the script system in order to make them referable from the script, and to register editors of several properties (for example, TfrxIBXTable.TableName) to make the work with the component more convenient.

It is better to store the engine’s registration code in a separate file with the RTTI suffix. See more about registration of classes in the script system in the corresponding chapter. Here is an example of such file:

unit frxIBXRTTI;

interface

{$I frx.inc}

implementation

uses

Windows, Classes, fs_iinterpreter, frxIBXComponents

{$IFDEF Delphi6}

, Variants

{$ENDIF};

type

TFunctions = class (TObject)

public

constructor Create;

destructor Destroy; override ;

end;

var

Functions: TFunctions;

{ TFunctions }

constructor TFunctions.Create;

begin

with fsGlobalUnit do

begin

AddedBy := Self;

AddClass(TfrxIBXDatabase, 'TfrxComponent');

AddClass(TfrxIBXTable, 'TfrxCustomDataset');

AddClass(TfrxIBXQuery, 'TfrxCustomQuery');

AddedBy := nil ;

end;

end;

destructor TFunctions.Destroy;

begin

if fsGlobalUnit <> nil then

fsGlobalUnit.RemoveItems(Self);

inherited ;

end;

initialization

Functions := TFunctions.Create;

finalization

Functions.Free;

end.

It is recommended to place the code of properties’ editors to a separate file with the Editor suffix as well. In our case, it is necessary to write editors to the TfrxIBXDatabase.DatabaseName, TfrxIBXTable.IndexName, TfrxIBXTable.TableName properties. See more about writing properties’ editors in the corresponding chapter.

Below is an example of such file:

unit frxIBXEditor;

interface

{$I frx.inc}

implementation

uses

Windows, Classes, SysUtils, Forms, Dialogs, frxIBXComponents, frxCustomDB,

frxDsgnIntf, frxRes, IBDatabase, IBTable

{$IFDEF Delphi6}

, Variants

{$ENDIF} ;

type

TfrxDatabaseNameProperty = class (TfrxStringProperty)

public

function GetAttributes: TfrxPropertyAttributes; override ;

function Edit: Boolean; override ;

end;

TfrxTableNameProperty = class (TfrxStringProperty)

public

function GetAttributes: TfrxPropertyAttributes; override ;

procedure GetValues; override ;

end;

TfrxIndexNameProperty = class (TfrxStringProperty)

public

function GetAttributes: TfrxPropertyAttributes; override ;

procedure GetValues; override ;

end;

{ TfrxDatabaseNameProperty }

function TfrxDatabaseNameProperty.GetAttributes: TfrxPropertyAttributes;

begin

{ this property possesses the editor }

Result := [paDialog];

end;

function TfrxDatabaseNameProperty.Edit: Boolean;

var

SaveConnected: Bool;

db: TIBDatabase;

begin

{ get a link to the TfrxIBXDatabase.Database }

db := TfrxIBXDatabase(Component).Database;

{ create a standard OpenDialog }

with TOpenDialog.Create( nil ) do

begin

InitialDir := GetCurrentDir;

{ we are interested in *.gdb files }

Filter := frxResources.Get('ftDB') + ' (*.gdb)|*.gdb|' +

frxResources.Get('ftAllFiles') + ' (*.*)|*.*';

Result := Execute;

if Result then

begin

SaveConnected := db.Connected;

db.Connected := False;

{ if a dialogue is completed successfully, assign a new DB name }

db.DatabaseName := FileName;

db.Connected := SaveConnected;

end;

Free;

end;

end ;

{ TfrxTableNameProperty }

function TfrxTableNameProperty.GetAttributes: TfrxPropertyAttributes;

begin

{ the property represents the list of values }

Result := [paMultiSelect, paValueList];

end;

procedure TfrxTableNameProperty.GetValues;

var

t: TIBTable;

begin

inherited ;

{ get a link to the TIBTable component }

t := TfrxIBXTable(Component).Table;

{ fill the list of tables available }

if t.Database <> nil then

t.DataBase.GetTableNames(Values, False);

end;

{ TfrxIndexProperty }

function TfrxIndexNameProperty.GetAttributes: TfrxPropertyAttributes;

begin

{ the property represents the list of values }

Result := [paMultiSelect, paValueList];

end;

procedure TfrxIndexNameProperty.GetValues;

var

i: Integer;

begin

inherited ;

try

{ get a link to the TIBTable component }

with TfrxIBXTable(Component).Table do

if (TableName <> '') and (IndexDefs <> nil ) then

begin

{ update indexes }

IndexDefs.Update;

{ fill the list of indexes available }

for i := 0 to IndexDefs.Count - 1 do

if IndexDefs[i].Name <> '' then

Values.Add(IndexDefs[i].Name);

end;

except

end ;

end;

initialization

frxPropertyEditors.Register(TypeInfo( String ), TfrxIBXDataBase, 'DatabaseName', TfrxDataBaseNameProperty);

frxPropertyEditors.Register(TypeInfo( String ), TfrxIBXTable, 'TableName', TfrxTableNameProperty);

frxPropertyEditors.Register(TypeInfo( String ), TfrxIBXTable, 'IndexName', TfrxIndexNameProperty);

end.